package com.study.vueblog.common;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.CollectionUtils;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {
                        MappedStatement.class,
                        Object.class,
                        RowBounds.class,
                        ResultHandler.class
                }
        ),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {
                        MappedStatement.class,
                        Object.class
                }
        )
})
@Slf4j
/**
 * 自定义MyBatis拦截器
 *
 * @since 2021-4-14
 */
public class MyBatisInterceptor implements Interceptor {

    /**
     * intercept 方法用来对拦截的SQL进行具体的操作
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("Execute the intercept method: {}",invocation.toString());
        try {
            // 获取xml中的一个select、update、insert、delete节点，是一条SQL语句
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            Object parameter = null;
            if (invocation.getArgs().length >1) {
                parameter = invocation.getArgs()[1];
            }
            // 获取到节点的id，即SQL语句的id
            String sqlId = mappedStatement.getId();
            // BoundSql就是封装mybatis最终产生的SQL类
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            //获取节点的配置
            Configuration cOnfiguration = mappedStatement.getConfiguration();
            //获取到最终的SQL语句
            String sql = getSql(cOnfiguration,boundSql,sqlId);
            String name1 = System.lineSeparator()+sqlId;
            String name2 = System.lineSeparator()+sql;
            log.info("Log:{}",name1);
            log.info("Log:{}",name2);
        }catch (RuntimeException e){
            log.error("An error is reported when the intercept method is executed:{}",e.getMessage());
        }
        return invocation.proceed();
    }

    /**
     * 封装一下SQL语句，是的结果返回完整的xml路径下的SQL语句节点id+SQL语句
     * @param configuration
     * @param boundSql
     * @param sqlId
     * @return
     */
    public static String getSql(Configuration configuration,BoundSql boundSql,String sqlId){
        String sql = showSql(configuration,boundSql);
        return sql;
    }

    /**
     * 如果参数是String，则添加单引号；
     * 如果是日期，则转换为时间格式器并加单引号；
     * 对参数是null和不是null的情况做了处理
     * @param obj
     * @return
     */
    private static String getParameterValue(Object obj){
        String value = null;
        if (obj instanceof String){
            value = "" + obj.toString();
        }else if(obj instanceof Date){
            DateFormat dateformat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,DateFormat.DEFAULT, Locale.CHINA);
            value = "" + dateformat.format(new Date()) + "" ;
        }else {
            if (obj != null){
                value = obj.toString();
            }else {
                value = "null";
            }
        }
        return value;
    }

    /**
     * 进行'?'的替换
     * @param configuration
     * @param boundSql
     * @return
     */
    public static String showSql(Configuration configuration,BoundSql boundSql){
        //获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //SQL语句中多个空格都用一个空格代替
        String sql = boundSql.getSql();
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null){
            //获取类型处理器注册器，类型处理器的功能是进行java类型和数据库类型的转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            //如果parameterObject.getClass()可以找到对应的类型，则替换
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){
                sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(parameterObject)));
            }else{
                //MetaObject主要是封装了originalObject对象，提供了get和set方法用于获取和设置originalObject的属性值，
                // 主要支持JavaBean、Collection、Map三种类型的操作
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings){
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)){
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(obj)));
                    }else if(boundSql.hasAdditionalParameter(propertyName)){
                        //该分支是动态SQL
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(obj)));
                    }else {
                        //打印出确实，提醒该参数确实并防止错误
                        sql = sql.replaceFirst("\\?","缺失");
                    }
                }
            }
        }
        return sql;
    }

    @Override
    public Object plugin(Object target){
        if (target instanceof Executor){
            return Plugin.wrap(target,this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties){
        //获取属性
        log.info("properties method:{}",properties.toString());
    }

}
